package com.one.pass.config;

import com.one.pass.service.RedisService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.UUID;

import static java.util.Optional.ofNullable;
import static javax.script.ScriptContext.ENGINE_SCOPE;

/*

- `RedisLockAspect`是一个切面类，用于处理带有`@RedisLock`注解的方法。
        - 在方法执行前，尝试获取Redis锁，获取锁成功后执行方法，方法执行完毕后释放锁。
        - 根据`@RedisLock`注解中的`lockType`属性选择不同的锁策略：
        - `WAIT`: 等待获取锁。
        - `NO_WAIT`: 不等待获取锁。
        - `WAIT_THROW_EXCEPTION`: 等待获取锁，如果超时则抛出异常。
        - `NO_WAIT_THROWS_EXCEPTION`: 不等待获取锁，如果获取失败则抛出异常。
*/

@Aspect  // 标识这是一个切面类
@Component  // 表示这是一个Spring管理的组件
@Slf4j  // 使用lombok的Slf4j注解，自动生成一个名为log的日志对象
@Order(value = Integer.MIN_VALUE + 1)  // 指定切面的顺序，值越小优先级越高
public class RedisLockAspect {

    // 线程本地变量，用于存储ScriptEngine实例
    private static final ThreadLocal<ScriptEngine> scriptEngineThreadLocal = new ThreadLocal<>();

    @Autowired  // 自动注入RedisService
    private RedisService redisService;

    // 创建并返回ScriptEngine实例
    @SneakyThrows
    public static ScriptEngine instanceScriptEngine() {
        ScriptEngine scriptEngine = scriptEngineThreadLocal.get();
        if (scriptEngine == null) {
            ScriptEngineManager engineManager = new ScriptEngineManager();
            scriptEngine = engineManager.getEngineByName("nashorn");
            scriptEngineThreadLocal.set(scriptEngine);
            scriptEngine.eval("function isEmpty(s){return s == undefined || s == ''}");
        }
        return scriptEngine;
    }

    // 清理参数
    public static void clearParams() {
        ofNullable(scriptEngineThreadLocal.get())
                .map(scriptEngine -> scriptEngine.getBindings(ENGINE_SCOPE))
                .ifPresent(Map::clear);
    }

    // 定义一个切点，匹配使用了@RedisLock注解的方法
    @Pointcut("@annotation(com.one.pass.config.RedisLock)")
    public void web() {
    }

    // 环绕通知，在方法执行前后进行处理
    @Around("web()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object service = joinPoint.getTarget();  // 获取目标对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();  // 获取方法签名
        Method method = signature.getMethod();  // 获取目标方法
        Object[] objects = joinPoint.getArgs();  // 获取方法参数
        RedisLock redisLock = method.getAnnotation(RedisLock.class);  // 获取方法上的@RedisLock注解
        ScriptEngine scriptEngine = instanceScriptEngine();  // 获取ScriptEngine实例
        for (int i = 0; i < method.getParameters().length; i++) {
            Parameter parameter = method.getParameters()[i];
            scriptEngine.put(parameter.getName(), objects[i]);  // 将参数名和参数值放入ScriptEngine中
        }
        String lockKey = scriptEngine.eval(redisLock.lockKey()) + "";  // 解析锁的键
        String lockValue = UUID.randomUUID().toString();  // 生成唯一的锁值
        try {
            switch (redisLock.lockType()) {
                case WAIT:
                    if (!redisService.tryLockWait(lockKey, lockValue, redisLock.time())) {
                        log.info("{}未获取到锁", redisLock.prefix());
                        return null;  // 获取锁失败，返回null
                    }
                    break;
                case NO_WAIT:
                    if (!redisService.tryLock(lockKey, lockValue, redisLock.time())) {
                        log.info("{}未获取到锁", redisLock.prefix());
                        return null;  // 获取锁失败，返回null
                    }
                    break;
                case WAIT_THROW_EXCEPTION:
                    if (!redisService.tryLockWait(lockKey, lockValue, redisLock.time())) {
                        log.info("{}未获取到锁", redisLock.prefix());
                        throw new RuntimeException(redisLock.error());  // 获取锁失败，抛出异常
                    }
                    break;
                case NO_WAIT_THROWS_EXCEPTION:
                    if (!redisService.tryLock(lockKey, lockValue, redisLock.time())) {
                        log.info("{}未获取到锁", redisLock.prefix());
                        throw new RuntimeException(redisLock.error());  // 获取锁失败，抛出异常
                    }
                    break;
            }
            return joinPoint.proceed();  // 执行目标方法
        } finally {
            clearParams();  // 清理参数
            redisService.releaseLock(lockKey, lockValue);  // 释放锁
        }
    }
}
